iT邦幫忙

2021 iThome 鐵人賽

DAY 16
0
Modern Web

初階 Rails 工程師的養成系列 第 16

Day16. Service, Strategy and Singleton Pattern

  • 分享至 

  • xImage
  •  

設計流程的出現,讓我們可以寫出一套好的流程,並且幫助團隊少寫多餘的程式碼。由於Ruby不像Javascript,是標準的物件導向語言,當然也可以使用各種形式的設計流程。今天,我們會介紹常用、漂亮的設計流程。

Service pattern

我們先舉以下為寄發簡訊的 Message服務為例

class Sms::SendMessage
  def self.call(phone, code: nil)
    new(phone, code).call
  end

  def initialize(phone, code)
    # @host, @username, @password 為存在專案內部的加密變數
    @host = Rails.application.credentials.sms[:host]
    @username = Rails.application.credentials.sms[:username]
    @password = Rails.application.credentials.sms[:password]
    # @dstaddr: 電話號碼
    # @smbody: 簡訊欸榮
    @dstaddr = phone
    @smbody = "#{code} is your verification code."
  end

  attr_reader :host, :username, :password, :dstaddr, :smbody

  def call
    ApiClient.post host, headers: nil, payload: { username: username, password: password, dstaddr: dstaddr, smbody: smbody }
  end
end

其中ApiClientDay12提到過,是專門打API用的類別方法。

Sms::SendMessage使用方式如下,第一個參數為欲寄發簡訊的電話號碼,第二個參數為驗證碼。

Sms::SendMessage.call('0983168969', code: '123456')

只要填妥兩個參數,就可以發簡訊了!

我們習慣使用call表示我們要呼叫該參數,而call 裡面包含呼叫實體變數,而這種行為我們稱為委託delegate。將self.new(*params).call的行為委託給call去執行的設計流程,稱為Service Pattern

def self.call(phone, code: nil)
  # self.new(phone, code).call 的簡寫
  new(phone, code).call
end

像這種單一class負責單一職責的設計模式Service Pattern, 是Rails常見的設計模式之一。

自己曾做過的電商專案中,除了簡訊發送Service以外,還有以下這些 Service。

第三方相關
  金流相關
    綠界付款
    綠界退款
    綠界開立發票
    綠界折讓發票
    綠界查詢付款狀態
  物流相關
    順豐物流下單
    順豐物流查詢貨態
  簡訊相關
    發送驗證碼

電商相關
  付款動作
  退款動作
  POS給點動作

這些Service的目的只為單一職責(一次只做一件事)。舉例來說因此若排程端、後台使用者端、顧客端需要做退款動作的時候,只需要呼叫退款流程 service即可。而這些ServiceMVC核心架構耦合性越低,重複使用的可能性跟便利性就更高,在同個專案所使用的Service 多和 ActiveRecord耦合,若要成為給大家復用的Gem,那就必須寫出耦合性更低的程式碼。

舉簡單的例子講解Service Pattern

class AppleService
  def self.call(*args, &block)
    new(*args).call(&block)
  end

  def initialize(a, b = nil, options = {})
    @a, @b,@c = a, b, options[:c]
  end

  attr_reader :a, :b, :c

  def call
    p "=== what is a: #{a} ==="
    p "=== what is b: #{b} ==="
    p "=== what is c: #{c} ==="
    
    if block_given?
      yield(a, b, c) 
    else
      {a: a, b: b, c: c}
    end
  end
end

至於如何使用參數,我們在Day11提及過,這裡就先簡單舉例AppleService 帶入不同參數的結果。

AppleService.call("a")             #=> {:a=>"a", :b=>nil, :c=>nil}
AppleService.call("a", c: 1)       #=> {:a=>"a", :b=>{:c=>1}, :c=>nil}
AppleService.call("a", nil, c: 1)  #=> {:a=>"a", :b=>nil, :c=>1}

我們也可以搭配 block 使用(Block 的使用可以參考Day9Day10

以下區塊負責的內容是將收到的三個值用斜線接起來再回傳,區塊是個有趣的章節,推薦讀者在學Rails的時候切萬不要退避三舍

AppleService.call("a", "b", c: 1) {|x,y,z| [x,y,z].join('/')} #=> "a/b/1"

Singleton Pattern

當我們對某物件新增單體方法,只有該物件有獨特技能,而若將某類別新增單體方法的話,則為類別方法。Day12Day13 提過,這邊就不細談

Strategy Pattern

這是我個人常使用的Pattern之一,不管是打Api、串接金流、串接物流、匯入表單等變化性很低(偶爾才有變化)的動作,都可以用Strategy Pattern 來去實現!Strategy Pattern可以用來設定一套流程,我們舉一個不存取值的例子來講:

module Strategy
  DEFAULT_STRATEGY = {
    params: -> { p '組成資料' },
    perform: -> { p '打Api' },
    receiver: -> { p '成功或失敗' },
    handler: -> { p '取得 response payload 並處理' }
  }
  
  def get_sty_flow
    block_given? ? yield(DEFAULT_STRATEGY) : DEFAULT_STRATEGY
  end
  
  def sty_flow
    get_sty_flow.map do |k, v|
      # 先找有沒有前綴為sty的方法
      go_method = "sty_#{k}".to_sym
      # 沒有的話取得 get_sty_flow 的 key
      go_method = v if !self.respond_to?(go_method)
      next unless go_method.present?
      
      # 判斷是否是Proc,若不是則視為 method
      if go_method.is_a? Proc
        go_method.call
      else
        self.send(go_method) 
      end
      
      # 回傳key(無意義)
      k
    end
  end
end

看到 DEFAULT_STRATEGY 對應 Proc,就有一種寫Javascript function可以當作變數的感覺。

說明: 

1. 流程會寫在 get_sty_flow 方法內,而DEFAULT_STRATEGY 為預設的流程
2. 執行流程的地方在 sty_flow
3. DEFAULT_STRATEGY 的value可以是Proc,也可以是symbol

接著我們介紹以下三種情境。

情境A ➡️ 使用預設的Strategy

class A
  include Strategy
end

A.new.sty_flow
#
#======== 印出以下資訊 ========#
# 
#   "組成資料", 
#   "打Api", 
#   "成功或失敗", 
#   "取得 response payload 並處理"
#
#=> [:params, :perform, :receiver, :handler]

情境B ➡️ 使用繼承,增添客製化的流程processor

class B
  include Strategy
  
  def get_sty_flow
    super do |strategy|
      { **strategy, processor: -> {'資料處理'} } 
    end
  end
end

B.new.sty_flow
#
#======== 印出以下資訊 ========#
#
#   "組成資料", 
#   "打Api", 
#   "成功或失敗", 
#   "取得 response payload 並處理"
#
#=> [:params, :perform, :receiver, :handler, :processor]

情境C ➡️ 使用客製化的方法 sty_params

class C
  include Strategy
  
  def sty_params
    p '在C組成資料,而非在預設流程'
  end
end

C.new.sty_flow
#
#======== 印出以下資訊 ========#
# 
#   "在C組成資料,而非在預設流程", 
#   "打Api", 
#   "成功或失敗", 
#   "取得 response payload 並處理"
#
#=> [:params, :perform, :receiver, :handler]

https://ithelp.ithome.com.tw/upload/images/20210914/20115854Ipakf00UGW.png

我們可以發現上面的方法是真的都可以被抽換的,符合 Strategy Pattern 的精神,也很Gurustrategy網站所使用的icon

結論

其實還有很多設計流程沒有講到,但礙於篇幅的關係,今天先講三個。在Day32講解攤提時,我們會介紹另一個設計流程Decorator Pattern

Class 系列到此告一個段落,明天開始講Dynamic Programming

參考資料


上一篇
Day15. Inheritance & Super - Ruby 繼承 part2
下一篇
Day17. Dynamic Programming
系列文
初階 Rails 工程師的養成34
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言